/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2021-2023. All rights reserved.
 *
 */

import fs, { Stats } from 'fs';
import fsp from 'fs/promises';
import path from 'path';

import { Json5Reader } from '@ohos/hvigor';
import { Obfuscation, ObfuscationDependencies, ObfuscationOptions } from '@ohos/hvigor-arkts-compose';
import { PackageJson } from 'type-fest';

import { buildOptionPath } from '../common/build-option-path-info.js';
import { CommonConst, CommonNodeConst } from '../const/common-const.js';
import { ModuleType } from '../enum/module-type-enum.js';
import { CoreModuleModelImpl } from '../model/module/core-module-model-impl.js';
import { ModuleModel } from '../model/module/module-model.js';
import { buildOptionManager } from '../project/build-option/build-mode-manager.js';
import { DependencyType } from '../project/dependency/core/dependency-interface.js';
import { ModuleTaskService } from '../tasks/service/module-task-service.js';
import { TargetTaskService } from '../tasks/service/target-task-service.js';
import { pathParamsOpt } from '../validate/dependency-tag-validate.js';
import { FileUtil } from './file-util.js';
import { OhosLogger } from './log/ohos-logger.js';

const _log: OhosLogger = OhosLogger.getLogger('obfuscation');

/**
 * 获取ObfuscationOptions入口
 * 需要在hvigor-arkts-compose中补充sdkApis
 * 仅需生成obfuscation.txt时不需sdkApis
 *
 * @param service
 * @param obfuscation
 * @param cacheDir 混淆缓存目录
 */
export const resolveObfuscationOptions = async (service: TargetTaskService, obfuscation: Obfuscation, cacheDir: string) => {
  const moduleModel = service.getTargetData().getModuleModel();
  const targetName = service.getTargetData().getTargetName();
  if (!obfuscation) {
    return undefined;
  }
  const dependencies: ObfuscationDependencies = await resolveObfuscationDependencies(moduleModel, service.getModuleService());
  obfuscation.ruleOptions = {
    enable: obfuscation.ruleOptions?.enable,
    rules:
      obfuscation.ruleOptions?.rules ||
      transformRules(obfuscation.ruleOptions?.files, moduleModel.getProjectDir(), moduleModel as CoreModuleModelImpl, targetName),
  };
  obfuscation.consumerRules =
    obfuscation.consumerRules || transformRules(obfuscation.consumerFiles, moduleModel.getProjectDir(), moduleModel as CoreModuleModelImpl, targetName);
  return {
    selfConfig: obfuscation,
    sdkApis: [],
    obfuscationCacheDir: cacheDir,
    exportRulePath: service.getTargetData().getPathInfo().getObfuscationExportRulePath(),
    dependencies,
  } as ObfuscationOptions;
};

export const resolveObfuscationDependencies = async (moduleModel: ModuleModel, moduleTaskService: ModuleTaskService): Promise<ObfuscationDependencies> => {
  // 解析当前模块oh-package.json/package.json
  const dependencies: ObfuscationDependencies = { libraries: [], hspLibraries: [], hars: [], hsps: [] };
  await resolveDepObfuscationOptions(moduleModel, moduleTaskService, dependencies);
  return dependencies;
};

const resolveDepObfuscationOptions = async (moduleModel: ModuleModel, moduleTaskService: ModuleTaskService, dependencies: ObfuscationDependencies) => {
  const isOhpm: boolean = moduleModel.getParentProject().isOhpmProject();
  const paramWhiteList: string[] = ['version', 'devDependencies', 'dynamicDependencies', 'dependencies'];
  const packagePath: string = isOhpm ? moduleModel.getOhPackageJson5Path() : moduleModel.getPackageJsonPath();
  if (!fs.existsSync(packagePath)) {
    return;
  }
  const depJsonObj: PackageJson = await Json5Reader.readJson5File(packagePath);
  if (isOhpm) {
    const parameterFileObj = moduleModel.getParentProject().getParameterFileObj();
    const pathParamsObj: pathParamsOpt = {
      parameterFilePath: moduleModel.getParentProject().getParameterFileAbsolutePath(),
      ohPackageFilePath: packagePath,
    };
    FileUtil.resolveJsonObjWithoutParam(depJsonObj, parameterFileObj, pathParamsObj, paramWhiteList);
  }

  if (!depJsonObj?.name) {
    return;
  }
  const packageDir = isOhpm ? CommonConst.OH_MODULES : CommonNodeConst.NODE_MODULES;

  // 遍历dependencies中声明的依赖
  for (const key in depJsonObj.dependencies) {
    if (!Object.prototype.hasOwnProperty.call(depJsonObj?.dependencies, key)) {
      continue;
    }

    // 从模块下oh_modules/node_modules中查找依赖的链接symbolic link
    const depDir: string = path.resolve(moduleModel.getProjectDir(), packageDir, ...key.split('/'));
    if (!fs.existsSync(depDir)) {
      _log.warn(`Dependency ${key} not found.`);
      continue;
    }
    let stats: Stats | undefined;
    try {
      stats = await fsp.lstat(depDir);
    } catch (e) {
      _log.warn(`Dependency ${key} not found or symbolic link is invalid.`);
      continue;
    }
    const realDir = stats.isSymbolicLink() ? await fsp.realpath(depDir) : depDir;

    // 根据依赖真实路径检查是否本地模块, 如拷贝到oh_modules下而非链接, 无法识别
    const depModuleModel: ModuleModel | undefined = resolveModuleWithPath(realDir, moduleModel.getAllModules());
    if (!depModuleModel) {
      // 如果不是本地模块, 检查是否存在obfuscation.txt，分为远程har模块和远程hsp模块
      const depObfuscationFile: string = path.resolve(realDir, CommonConst.OBFUSCATION_TXT);
      // 判断是当前远程模块moduleType
      const dependencyType = moduleTaskService.getAllDependencyType().get(key);
      if (fs.existsSync(depObfuscationFile)) {
        _log.debug(`Collect obfuscation config from dependency ${key}.`);
        if (dependencyType === DependencyType.DEPENDENCY_TYPE_HAR && dependencies.hars.indexOf(depObfuscationFile) === -1) {
          dependencies.hars.push(depObfuscationFile);
        }
        if (dependencyType === DependencyType.DEPENDENCY_TYPE_HSP && dependencies.hsps.indexOf(depObfuscationFile) === -1) {
          dependencies.hsps.push(depObfuscationFile);
        }
      }
      continue;
    }
    const depModuleType = depModuleModel.getModuleType();
    // 获取本地har模块buildOption
    const depBuildOption = buildOptionManager.getTargetBuildOption(depModuleModel.getName(), 'default');
    const obfuscation: Obfuscation | undefined = depBuildOption.arkOptions?.obfuscation;
    if (obfuscation) {
      // 重复判断
      if (dependencies.libraries.find((har) => har.libDir === realDir) || dependencies.hspLibraries.find((hsp) => hsp.libDir === realDir)) {
        continue;
      }
      _log.debug(`Collect obfuscation config from library ${depModuleModel.getName()}.`);
      const rule = {
        ruleOptions: {
          enable: obfuscation.ruleOptions?.enable,
          rules: transformRules(obfuscation.ruleOptions?.files, realDir, depModuleModel as CoreModuleModelImpl, 'default'),
        },
        consumerRules: transformRules(obfuscation.consumerFiles, realDir, depModuleModel as CoreModuleModelImpl, 'default'),
        libDir: realDir,
      } as Obfuscation;
      if (depModuleType === ModuleType.Har) {
        dependencies.libraries.push(rule);
      }
      if (depModuleType === ModuleType.Shared) {
        dependencies.hspLibraries.push(rule);
      }
    }
    if (!depModuleModel.isHarModule()) {
      continue;
    }

    // 此依赖为本地模块, 递归查找本地har和远程har
    // 只有本地模块递归去寻找
    await resolveDepObfuscationOptions(depModuleModel, moduleTaskService, dependencies);
  }
};

/**
 * 根据依赖根路径的真实路径判断是否本地模块
 *
 * @param realDir 依赖真实路径
 * @param moduleModels 所有模块
 */
const resolveModuleWithPath = (realDir: string, moduleModels: ModuleModel[]): ModuleModel | undefined => {
  for (const model of moduleModels.values()) {
    const relative: string = path.relative(model.getProjectDir(), realDir);
    if (relative === '') {
      return model as ModuleModel;
    }
  }
  return undefined;
};

/**
 * 转换混淆规则文件路径为绝对路径并检查是否存在
 *
 * @param rules 混淆规则文件
 * @param dir 模块根路径
 * @param module 模块
 * @param targetName
 */
export const transformRules = (rules: string | string[] | undefined, dir: string, module: CoreModuleModelImpl, targetName: string): string[] => {
  if (!rules) {
    return [];
  }
  const result: string[] = [];
  if (typeof rules === 'string') {
    checkObFile(rules, dir, result, module, targetName);
    return result;
  }
  rules.forEach((rule) => checkObFile(rule, dir, result, module, targetName));
  return result;
};

const checkObFile = (file: string, dir: string, result: string[], module: CoreModuleModelImpl, targetName: string) => {
  const realFile: string = path.resolve(dir, file);
  const readFilePath: string = buildOptionPath.getTargetBuildOptPath(module, targetName, 'files');
  if (!fs.existsSync(realFile)) {
    _log.printErrorExit('OBFUSCATION_FILE_LOST', [file, realFile, module.getName(), readFilePath], [[file, realFile]]);
  }
  result.push(realFile);
};
